package isDim;

import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import InterconectionSubsystem.Sender;
import InterconectionSubsystem.Address;
import org.json.JSONObject;

public class DynamicInterconectionManager implements Runnable{
	
	private BlockingQueue<dimCargo> mailbox;
	private BlockingQueue<List<Address>> estimationBox;
	private BlockingQueue<List<Address>> eventBox;
	private BlockingQueue<Boolean> dimPong;
	private Thread rd;
	private Thread uch;
	private String ac_ip;
	private int ac_port;
	private List<dimAirport> airports;
	private Map<Integer,List<String>> flights;
	private List<Integer> localFlights;
	private LocalConfig config;
	private int currentId;
	private dimUpdateId lastUpdate;
	private Lock lock;
	
	private final String SQL_ACTIVE_FLIGHTS = "SELECT flightId FROM Planes WHERE cancelled = false;";
	
public DynamicInterconectionManager(BlockingQueue<dimCargo> box, BlockingQueue<List<Address>> evBox, BlockingQueue<List<Address>> esBox, BlockingQueue<Boolean> dPong,
			LocalConfig localconfig)
	{
		config = localconfig; 
		rd = new Thread(new dimReciver(box, config.dimPort()));
		mailbox = box;
		estimationBox = esBox;
		eventBox = evBox;
		dimPong = dPong;
		ac_ip = config.trackerIp();
		ac_port = config.trackerPort();
		localFlights = new LinkedList<Integer>();
		currentId = (new java.util.Random()).nextInt();
		airports = new LinkedList<dimAirport>();
		flights = new HashMap<Integer,List<String>>();
		
		lastUpdate = new dimUpdateId();
		lock = new ReentrantLock();
		uch = new Thread(new dimUpdateConfirmationHandler(lastUpdate, lock, mailbox));
		
		try{
			mailbox.put(new dimCargo(dimCargo.Type.STARTUP));
		}catch(Exception e){ System.out.println("Error adding STARTUP message to dimBox: " + e.getMessage()); }
	}

	public void run()
	{
		// Start dimReciver thread.
		rd.start();
		uch.start();
		
		while(true)
		{
			// Take next message.
			dimCargo msg;
			try{
				msg = mailbox.take();
			}catch(Exception e){ System.out.print("Error taking task on Dynamic Interconection Manager: " + e.getMessage() + "\n"); continue; }
			
			System.out.println("DIM: Mensaje entrante.");
			
			// Pass it to correspondent handler.
			switch(msg.type())
			{
				case AC_MSG:
					handle_ac_msg(msg.message());
					break;
				case REQUEST:
					handle_request(msg.kind(), msg.flight());
					break;
				case NEW_FLIGHT:
					handle_new_flight(msg.flight());
					break;
				case DEL_FLIGHT:
					handle_del_flight(msg.flight());
					break;
				case STARTUP:
					handle_startup();
					break;
				case PING:
					handle_ping(msg.flight());
					break;
				case FORCE_UPDATE:
					update();
					break;
			}
		}
	}
	
// ******* From here, handling functions **************
	
	private void handle_ac_msg(String msg)
	{
		System.out.print("DIM: Mensaje del tracker...");
		
		// Read AirCoordination message.
		JSONObject detail = null;
		try{
			detail = new JSONObject(msg);
		}catch(Exception e){ System.out.print("Error reading AC message: " + e.getMessage() + "\n"); return; }
		
		// Decode AirCoordination message.
		String type = null;
		int id = 0;
		try{
			type = detail.getString("type");
			id = detail.getInt("id");
		}catch(Exception e){ System.out.print("Error decoding AC message: " + e.getMessage() + "\n"); return; }
		
		// Send message details to correspondent handler.
		if (type.equals("PING"))
		{
			System.out.println("PING.OK.");
			send_ok(id);
		}
		
		if (type.equals("SYNC"))
			handle_sync(detail, id);
		
		if (type.equals("S_UNPLUG"))
			handle_unplug(detail, id);
		
		if (type.equals("OK"))
		{
			lock.lock();
			if (id == lastUpdate.id())
				lastUpdate.ok(true);
			lock.unlock();
		}
	}
	
	private void handle_request(dimCargo.Kind kind, int flight)
	{
		System.out.print("DIM: Request...");
		List<Address> list = null;
		
		switch(kind)
		{
			case EVENT:
				System.out.println("del event exchanger.");
				list = handle_event_request(flight);
				eventBox.add(list);
				break;
			case ESTIMATION:
				System.out.println("del estimation exchanger.");
				list = handle_estimation_request(flight);
				estimationBox.add(list);
				break;				
		}
		System.out.println("REQUEST OK.");
	}
	
	private void handle_new_flight(int flight)
	{
		System.out.println("DIM: Vuelo nuevo.");
		// Add flight to local flights list. (Ordered)
		int position = Collections.binarySearch(localFlights, flight);
		if (position < 0){
			localFlights.add(-position-1, flight);

			// Send update to coordination.
			update();
		}
		else System.out.println("WARNING. Attempting to add an existent flight. Skipping.");
		System.out.println("NEW_FLIGHT OK.");
	}
	
	private void handle_del_flight(int flight)
	{
		System.out.println("DIM: Vuelo a eliminar.");
		// Delete flight from local flights list.
		int position = Collections.binarySearch(localFlights, flight);
		if (position >= 0)
		{
			localFlights.remove(position);
			
			// Send update to coordination.
			update();
		}
		else System.out.println("WARNING. Attempting to erase an inexistent flight. Skipping.");
		System.out.println("DEL_FLIGHT OK.");
	}
	
	private void handle_startup()
	{
		System.out.println("DIM: Pedido de incialización.");
		retrieve_local_flights();
		update();
		System.out.println("STARTUP OK.");
	}
	
	private void handle_ping(int id)
	{
		System.out.println("DIM: Ping.");
		try{
			dimPong.put(true);
		}catch(Exception e){ System.out.print("Error responding dimPing: " + e.getMessage()); }
		System.out.println("PING OK.");
	}

// ******* From here, sub-handling functions **************	
	
	private void handle_sync(JSONObject detail, int id)
	{
		System.out.println("SYNC.");
		String ip = null;
		int evPort = 0;
		int esPort = 0;
		int nFlights = 0;
		List<Integer> cflights = new LinkedList<Integer>();

		// Retrieve data from message.
		try{
			ip = detail.getString("ip");
			evPort = detail.getInt("evPort");
			esPort = detail.getInt("esPort");
			nFlights = detail.getInt("nFlights");
			for (int i=0; i<nFlights; ++i)
				cflights.add(detail.getInt("fp"+Integer.toString(i)));
		}catch(Exception e){ System.out.print("Error decoding sync: " + e.getMessage() + "\n"); return; }
		
		// Send OK.
		send_ok(id);
		
		// Add or update remote airport data on local data structure.
		dimAirport airport = new dimAirport(ip, evPort, esPort);
		int position = Collections.binarySearch(airports, new dimAirport(ip));
		if (position >= 0)
		{
			airports.remove(position);
			airports.add(position, airport);
		}
		else
		{
			airports.add(-position-1, airport);
		}
		
		// Update flights map.
		Iterator<Integer> iter = cflights.iterator();
		while (iter.hasNext())
		{
			int current_flight = iter.next();
			List<String> fairports = flights.get(current_flight);
			if (fairports != null)
				fairports.add(ip);
			else
			{
				fairports = new LinkedList<String>();
				fairports.add(ip);
				flights.put(current_flight, fairports);
			}
		}
		System.out.println("SYNC OK.");
	}

	private void handle_unplug(JSONObject detail, int id)
	{
		System.out.println("UNPLUG.");
		String ip = null;
		
		// Retrieve data from message.
		try{
			ip = detail.getString("ip");
		}catch(Exception e){ System.out.print("Error decoding sync: " + e.getMessage() + "\n"); return; }
		
		// Send OK.
		send_ok(id);
		
		// Delete remote airport from local data structure.
		int position = Collections.binarySearch(airports, new dimAirport(ip));
		if (position >= 0)
			airports.remove(position);
		
		// Update flight map.
		Iterator<Integer> iter = flights.keySet().iterator();
		while (iter.hasNext())
		{
			int current_flight = iter.next();
			List<String> fairports = flights.get(current_flight);
			fairports.remove(ip);
			if (fairports.isEmpty())
				flights.remove(current_flight);
		}
		System.out.println("UNPLUG OK.");
	}
	
	private void send_ok(int id)
	{
		// Generate OK message.
		JSONObject message = null;
		try{
			message = new JSONObject();
			message.put("type", "OK");
			message.put("id", id);
		}catch(Exception e){ System.out.print("Error generating ok answer: " + e.getMessage() + "\n"); return; }
		
		// Send previously generated OK message.
		Sender.send(ac_ip, ac_port, message.toString());
	}
	
	
	private void retrieve_local_flights()
	{
		Statement sql_statement = null;
		ResultSet sql_resultset = null;
		
		// Retrieve stored local flights data.
		try{
			sql_statement = DriverManager.getConnection(String.format("jdbc:mysql://%s/%s", config.sysDBserver(), config.airportDatabase()), config.airportUser(), config.airportPass()).createStatement();
			sql_resultset = sql_statement.executeQuery(SQL_ACTIVE_FLIGHTS);
		}catch(Exception e){ System.out.println("Error retrieving local flights data."); return; }
		
		// Put retrieve data into local data structure.
		List<Integer> tmpLocalFlights = new LinkedList<Integer>();
		try{
			boolean valid = sql_resultset.first();
			while(valid)
			{
				int fid = sql_resultset.getInt("flightId");
				// localFlights must be ordered.
				int position = -Collections.binarySearch(tmpLocalFlights, fid)-1;
				tmpLocalFlights.add(position, fid);
				valid = sql_resultset.next();
			}
		}catch(Exception e){ System.out.println("Error retrieving local flights stored data: " + e.getMessage()); return; }
		
		// If everything gone ok, use retrieved new data.
		localFlights = tmpLocalFlights;
	}
	
	private void update()
	{
		int id = currentId++;
		
		// Generate UPDATE message with local data.
		JSONObject msg = null;
		msg = new JSONObject();
		try{
			msg.put("type", "UPDATE");
			msg.put("id", id);
			msg.put("ip", config.localIp());
			msg.put("dimPort", config.dimPort());
			msg.put("evPort", config.evPort());
			msg.put("esPort", config.esPort());
			msg.put("nFlights", localFlights.size());
			
			int nFlight = 0;
			Iterator<Integer> iter = localFlights.iterator();
			while (iter.hasNext())
			{
				int flightId = iter.next();
				msg.put("fp" + String.valueOf(nFlight), flightId);
				nFlight++;
			}
		}catch(Exception e){ System.out.println("Error generating update message: " + e.getMessage()); return; }

		// Send UPDATE message to Coordinator.
		Sender.send(ac_ip, ac_port, msg.toString());
		
		lock.lock();
		lastUpdate.set_id(id);
		lock.unlock();
	}
	
	private List<Address> handle_event_request(int flight)
	{
		List<String> ips = flights.get(flight);
		List<Address> addresses = new LinkedList<Address>();
		
		if (ips == null) return addresses;

		Iterator<String> iter = ips.iterator();
		while (iter.hasNext())
		{
			String ip = iter.next();
			int position = Collections.binarySearch(airports, new dimAirport(ip));
			if (position < 0) return null;
			dimAirport airport = airports.get(position);
			addresses.add(new Address(ip, airport.get_evPort()));
		}
		
		return addresses;
	}
	
	private List<Address> handle_estimation_request(int flight)
	{
		List<String> ips = flights.get(flight);
		List<Address> addresses = new LinkedList<Address>();
		
		if (ips == null) return addresses;
		
		Iterator<String> iter = ips.iterator();
		while (iter.hasNext())
		{
			String ip = iter.next();
			int position = Collections.binarySearch(airports, new dimAirport(ip));
			if (position < 0) return null;
			dimAirport airport = airports.get(position);
			addresses.add(new Address(ip, airport.get_esPort()));
		}
		
		return addresses;
	}
}